#!/usr/bin/env python3

# Copyright (c) 2020-2021 Fpemud <fpemud@sina.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

import os
import re
import enum
import pathlib
import screeninfo
# import pyamdgpuinfo       # FIXME
from .._const import Const
from ._ladder_diagram import LadderDiagram


class SysInfoType(enum.Enum):
    CPU = enum.auto()
    MEMORY = enum.auto()
    GRAPHICS = enum.auto()
    SOUND = enum.auto()
    CAMERA = enum.auto()
    INPUT = enum.auto()


class SysInfo:

    def __init__(self, flags=[]):
        if SysInfoType.CPU in flags:
            self.cpu = self._readCpu()
        if SysInfoType.MEMORY in flags:
            self.memory = self._readMemory()
        if SysInfoType.GRAPHICS in flags:
            self.graphics = self._readGraphics()
        if SysInfoType.SOUND in flags:
            self.audio_out = self._readSound()
        if SysInfoType.CAMERA in flags:
            self.camera = self._readCamera()
        if SysInfoType.INPUT in flags:
            self.input = self._readInput()

    def _readCpu(self):
        # note:
        # 1. we assume all CPUs are the same model if there's multiple CPUs installed
        # 2. we read /proc/cpuinfo directly instead of using dev-python/py-cpuinfo because the latter is too slow
        #

        buf = pathlib.Path("/proc/cpuinfo").read_text()

        vendor = "Unknown"
        arch = None
        if True:
            m = re.search(r'vendor_id\s*:\s*(\S+)', buf, re.M)
            if m is not None:
                if m.group(1) == "GenuineIntel":
                    vendor = "Intel"
                    arch = SysInfoCpu.Arch.X86_64
                if m.group(1) == "AuthenticAMD":
                    vendor = "AMD"
                    arch = SysInfoCpu.Arch.X86_64
            assert arch is not None

        model = "Unknown"
        if True:
            m = re.search(r'model name\s*:\s*(.*)', buf, re.M)
            if m is not None:
                # intel models
                if "i7-4600U" in m.group(1):
                    model = "i7-4600U"

                # amd models
                if "Ryzen Threadripper 1920X" in m.group(1):
                    model = "Ryzen Threadripper 1920X"
                if "Ryzen Threadripper 2990WX" in m.group(1):
                    model = "Ryzen Threadripper 2990WX"

        count = 1

        totalCoreCount = len(re.findall(r'processor\s+: [0-9]+', buf))

        return SysInfoCpu(vendor, model, arch, count, totalCoreCount)

    def _readMemory(self):
        memSize = None
        with open("/proc/meminfo", "r") as f:
            # Since the memory size shown in /proc/meminfo is always a
            # little less than the real size because various sort of
            # reservation, so we convert it to GB and do a "+1"
            m = re.search("^MemTotal:\\s+(\\d+)", f.read())
            memSize = (int(m.group(1)) // 1024 // 1024 + 1) * 1024 * 1024

        return SysInfoMemory(memSize)

    def _readGraphics(self):
        # note:
        # 1. we don't support multiple screens yet
        # 2. we assume all graphics cards are the same model if there's multiple graphics cards installed
        #

        screen = screeninfo.get_monitors()
        if True:
            if len(screen) != 1:
                raise SysInfoError("unable to find valid screen information")
            screen = screen[0]

        max_window_width = int(screen.width * 0.9)
        max_window_height = int(screen.height * 0.9)

        vendor = None
        model = None
        memSize = None
        count = None
        while True:
            # AMD first
            n = pyamdgpuinfo.detect_gpus()
            if n > 0:
                dev = pyamdgpuinfo.get_gpu(0)
                vendor = "AMD"
                model = dev.name if not dev.name.startswith("AMD ") else dev.name.replace("AMD ", "")
                memSize = dev.memory_info["vram_size"]
                count = 1                                   # FIXME
                break

            # Intel
            if True:
                vendor = "Intel"
                model = "Unknown"                           # FIXME
                memSize = -1                                # FIXME
                count = 1
                break

            # FIXME: we don't support NVIDIA
            assert False

        return SysInfoGraphics(screen.width_mm, screen.height_mm, screen.width, screen.height, max_window_width, max_window_height, vendor, model, memSize, count)

    def _readSound(self):
        return NotImplementedError()

    def _readCamera(self):
        return NotImplementedError()

    def _readInput(self):
        return NotImplementedError()


class SysInfoCpu:

    _cpuLadder = None

    class Arch(enum.Enum):
        X86 = "x86"
        X86_64 = "x86_64"
        ARM = "arm"
        ARM64 = "arm64"

    def __init__(self, vendor, model, arch, count, total_core_count):
        assert count >= 1
        assert total_core_count >= 1

        self.vendor = vendor
        self.model = model
        self.arch = arch
        self.count = count
        self.total_core_count = total_core_count

    def compare_with(self, vendor, model):
        if self._cpuLadder is None:
            self._cpuLadder = LadderDiagram(os.path.join(Const.DATA_DIR, "cpu.%s.ladder-diagram" % (self.arch)))

        assert self._cpuLadder.exists(vendor, model)

        if self._cpuLadder.exists(self.vendor, self.model):
            score1 = self._cpuLadder.get_score(self.vendor, self.model)     # note: get_score() returns float
            for i in range(1, self.count):
                score1 *= 1.6
            score2 = self._cpuLadder.get_score(vendor, model)

            if score1 == score2:
                return 0
            elif score1 > score2:
                return 1
            else:
                return -1
        else:
            return -1


class SysInfoMemory:

    def __init__(self, size):
        self.size = size


class SysInfoGraphics:

    class Display:

        def __init__(self, screen_width_mm, screen_height_mm, screen_width, screen_height, max_window_width, max_window_height):
            self.screen_width_mm = screen_width_mm
            self.screen_height_mm = screen_height_mm
            self.screen_width = screen_width
            self.screen_height = screen_height
            self.max_windows_width = max_window_width
            self.max_windows_height = max_window_height

        def filter_resolutions(self, resolutions, fullscreen_or_windowed):
            if fullscreen_or_windowed:
                ret = [x for x in resolutions if x.split("x")[0] <= self.screen_width and x.split("x")[1] <= self.screen_height]
            else:
                ret = [x for x in resolutions if x.split("x")[0] <= self.max_windows_width and x.split("x")[1] <= self.max_windows_height]

            # leave at least 1 element
            if len(ret) == 0:
                ret = [resolutions[0]]

            return ret

    class Gpu:

        _gpuLadder = None

        def __init__(self, vendor, model, mem_size, count):
            assert count >= 1
            assert mem_size > 0 or mem_size == -1           # FIXME

            self.vendor = vendor
            self.model = model
            self.mem_size = mem_size
            self.count = count

        def compare_with(self, vendor, model):
            if self._gpuLadder is None:
                self._gpuLadder = LadderDiagram(os.path.join(Const.DATA_DIR, "gpu.ladder-diagram"))

            assert self._gpuLadder.exists(vendor, model)

            if self._gpuLadder.exists(self.vendor, self.model):
                score1 = self._gpuLadder.get_score(self.vendor, self.model)     # note: get_score() returns float
                for i in range(1, self.count):
                    score1 *= 1.6
                score2 = self._gpuLadder.get_score(vendor, model)
                if score1 == score2:
                    return 0
                elif score1 > score2:
                    return 1
                else:
                    return -1
            else:
                return -1

    def __init__(self, screen_width_mm, screen_height_mm, screen_width, screen_height, max_window_height, max_window_width, vendor, model, mem_size, count):
        self.display = self.Display(screen_width_mm, screen_height_mm, screen_width, screen_height, max_window_height, max_window_width)
        self.gpu = self.Gpu(vendor, model, mem_size, count)


class SysInfoError(Exception):
    pass
